1 /**
2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 Normal appliation mode.
7 */
8 module app_normal;
9 
10 import std.algorithm : among;
11 import std.exception : collectException;
12 import logger = std.experimental.logger;
13 
14 import code_checker.cli : Config;
15 import code_checker.compile_db : CompileCommandDB;
16 import code_checker.types : AbsolutePath, Path, AbsoluteFileName;
17 
18 immutable compileCommandsFile = "compile_commands.json";
19 
20 int modeNormal(ref Config conf) {
21     auto fsm = NormalFSM(conf);
22     return fsm.run;
23 }
24 
25 private:
26 
27 /** FSM for the control flow when in normal mode.
28  */
29 struct NormalFSM {
30     enum State {
31         init_,
32         changeWorkDir,
33         checkForDb,
34         genDb,
35         checkGenDb,
36         fixDb,
37         checkFixDb,
38         runRegistry,
39         cleanup,
40         done,
41     }
42 
43     struct StateData {
44         int exitStatus;
45         bool hasGenerateDbCommand;
46         bool hasCompileDbs;
47     }
48 
49     State st;
50     Config conf;
51     bool removeCompileDb;
52     /// Root directory from which the program where initially started.
53     AbsolutePath root;
54     /// Exit status of used to indicate the success to the user.
55     int exitStatus;
56 
57     this(Config conf) {
58         this.conf = conf;
59     }
60 
61     int run() {
62         StateData d;
63         d.hasGenerateDbCommand = conf.compileDb.generateDb.length != 0;
64         d.hasCompileDbs = conf.compileDb.dbs.length != 0;
65 
66         while (st != State.done) {
67             debug logger.tracef("state: %s data: %s", st, d);
68 
69             st = next(st, d);
70             action(st);
71 
72             // sync with changed struct members as needed
73             d.exitStatus = exitStatus;
74         }
75 
76         return d.exitStatus;
77     }
78 
79     /** The next state is calculated. Only dependent on current state and state data.
80      *
81      * These clean depenencies should make it easier to reason about the flow.
82      */
83     static State next(const State curr, const StateData d) {
84         State next_ = curr;
85 
86         final switch (curr) {
87         case State.init_:
88             next_ = State.changeWorkDir;
89             break;
90         case State.changeWorkDir:
91             next_ = State.checkForDb;
92             break;
93         case State.checkForDb:
94             next_ = State.fixDb;
95             if (d.hasGenerateDbCommand)
96                 next_ = State.genDb;
97             break;
98         case State.genDb:
99             next_ = State.checkGenDb;
100             if (d.exitStatus != 0)
101                 next_ = State.cleanup;
102             break;
103         case State.checkGenDb:
104             next_ = State.fixDb;
105             if (d.exitStatus != 0)
106                 next_ = State.cleanup;
107             else if (d.hasCompileDbs)
108                 next_ = State.fixDb;
109             break;
110         case State.fixDb:
111             next_ = State.checkFixDb;
112             break;
113         case State.checkFixDb:
114             next_ = State.runRegistry;
115             if (d.exitStatus != 0)
116                 next_ = State.cleanup;
117             break;
118         case State.runRegistry:
119             next_ = State.cleanup;
120             break;
121         case State.cleanup:
122             next_ = State.done;
123             break;
124         case State.done:
125             break;
126         }
127 
128         return next_;
129     }
130 
131     void act_changeWorkDir() {
132         import std.file : getcwd, chdir;
133 
134         root = Path(getcwd).AbsolutePath;
135         if (conf.miniConf.workDir != root)
136             chdir(conf.miniConf.workDir);
137     }
138 
139     void act_checkForDb() {
140         import std.file : exists;
141 
142         removeCompileDb = !exists(compileCommandsFile) && !conf.compileDb.keep;
143     }
144 
145     void act_genDb() {
146         import std.process : spawnShell, wait;
147 
148         auto res = spawnShell(conf.compileDb.generateDb).wait;
149         if (res != 0) {
150             logger.error("Failed running command to generate the compile_commands.json");
151             exitStatus = 1;
152         }
153     }
154 
155     void act_fixDb() {
156         import std.algorithm : map;
157         import std.array : appender, array;
158         import std.stdio : File;
159         import code_checker.compile_db : fromArgCompileDb;
160 
161         logger.trace("Creating a unified compile_commands.json");
162 
163         auto compile_db = appender!string();
164         try {
165             auto dbs = findCompileDbs(conf.compileDb.dbs);
166             if (dbs.length == 0) {
167                 logger.errorf("No %s found in %s", compileCommandsFile, conf.compileDb.dbs);
168                 exitStatus = 1;
169                 return;
170             }
171 
172             auto db = fromArgCompileDb(dbs.map!(a => cast(string) a.dup).array);
173             unifyCompileDb(db, compile_db);
174             File(compileCommandsFile, "w").write(compile_db.data);
175         } catch (Exception e) {
176             logger.errorf("Unable to process %s", compileCommandsFile);
177             logger.error(e.msg);
178             exitStatus = 1;
179         }
180     }
181 
182     void act_runRegistry() {
183         import std.algorithm : map;
184         import std.array : array;
185         import code_checker.engine;
186         import code_checker.compile_db : fromArgCompileDb, parseFlag,
187             CompileCommandFilter;
188 
189         Environment env;
190         env.compileDbFile = AbsolutePath(Path(compileCommandsFile));
191         env.compileDb = fromArgCompileDb([env.compileDbFile]);
192         env.files = () {
193             if (conf.analyzeFiles.length == 0)
194                 return env.files = env.compileDb.map!(a => cast(string) a.absoluteFile.payload)
195                     .array;
196             else
197                         return conf.analyzeFiles.dup;
198         }();
199         env.genCompileDb = conf.compileDb.generateDb;
200         env.flagFilter = conf.compileDb.flagFilter;
201 
202         env.staticCode = conf.staticCode;
203         env.clangTidy = conf.clangTidy;
204         env.compiler = conf.compiler;
205         env.logg = conf.logg;
206 
207         Registry reg;
208         reg.put(new ClangTidy, Type.staticCode);
209         exitStatus = execute(env, reg) == Status.passed ? 0 : 1;
210     }
211 
212     void act_cleanup() {
213         import std.file : remove, chdir;
214 
215         if (removeCompileDb)
216             remove(compileCommandsFile).collectException;
217 
218         chdir(root);
219     }
220 
221     /// Generate a callback for each state.
222     void action(const State st) {
223         string genCallAction() {
224             import std.format : format;
225             import std.traits : EnumMembers;
226 
227             string s;
228             s ~= "final switch(st) {";
229             static foreach (a; EnumMembers!State) {
230                 {
231                     const actfn = format("act_%s", a);
232                     static if (__traits(hasMember, NormalFSM, actfn))
233                         s ~= format("case State.%s: %s();break;", a, actfn);
234                     else {
235                         pragma(msg, __FILE__ ~ ": no callback found: " ~ actfn);
236                         s ~= format("case State.%s: break;", a);
237                     }
238                 }
239             }
240             s ~= "}";
241             return s;
242         }
243 
244         mixin(genCallAction);
245     }
246 }
247 
248 auto findCompileDbs(const(AbsolutePath)[] paths) nothrow {
249     import std.algorithm : filter, map;
250     import std.file : exists, isDir, isFile, dirEntries, SpanMode;
251 
252     AbsolutePath[] rval;
253 
254     static AbsolutePath[] findRecursive(const AbsolutePath p) {
255         import std.path : baseName;
256 
257         AbsolutePath[] rval;
258         foreach (a; dirEntries(p, SpanMode.depth).filter!(a => a.isFile)
259                 .filter!(a => a.name.baseName == compileCommandsFile).map!(a => a.name)) {
260             try {
261                 rval ~= AbsolutePath(Path(a));
262             } catch (Exception e) {
263                 logger.warning(e.msg);
264             }
265         }
266         return rval;
267     }
268 
269     foreach (a; paths.filter!(a => exists(a))) {
270         try {
271             if (a.isDir) {
272                 logger.tracef("Looking for compilation database in '%s'", a).collectException;
273                 rval ~= findRecursive(a);
274             } else if (a.isFile)
275                 rval ~= a;
276         } catch (Exception e) {
277             logger.warning(e.msg).collectException;
278         }
279     }
280 
281     return rval;
282 }
283 
284 /// Unify multiple compilation databases to one json file.
285 void unifyCompileDb(AppT)(CompileCommandDB db, ref AppT app) {
286     import std.algorithm : map, joiner, filter, copy;
287     import std.array : array, appender;
288     import std.ascii : newline;
289     import std.format : formattedWrite;
290     import std.json : JSONValue;
291     import std.path : stripExtension;
292     import std.range : put;
293     import code_checker.compile_db;
294 
295     auto flag_filter = CompileCommandFilter(defaultCompilerFilter.filter.dup, 0);
296     logger.trace(flag_filter);
297 
298     void writeEntry(T)(ref const T e) {
299         import std.exception : assumeUnique;
300         import std.utf : byChar;
301 
302         auto raw_flags = () @safe{
303             auto app = appender!(string[]);
304             e.parseFlag(flag_filter).flags.copy(app);
305             // add back dummy -c otherwise clang-tidy do not work
306             ["-c", cast(string) e.absoluteFile].copy(app);
307             return app.data;
308         }();
309 
310         formattedWrite(app, `"directory": "%s",`, cast(string) e.directory);
311 
312         if (e.arguments.hasValue) {
313             formattedWrite(app, `"arguments": %s,`, raw_flags);
314         } else {
315             formattedWrite(app, `"command": "%-(%s %)",`, raw_flags);
316         }
317 
318         if (e.output.hasValue)
319             formattedWrite(app, `"output": "%s",`, cast(string) e.absoluteOutput);
320         formattedWrite(app, `"file": "%s"`, cast(string) e.absoluteFile);
321     }
322 
323     if (db.length == 0) {
324         return;
325     }
326 
327     formattedWrite(app, "[");
328 
329     foreach (ref const e; db[0 .. $ - 1]) {
330         formattedWrite(app, "{");
331         writeEntry(e);
332         formattedWrite(app, "},");
333         put(app, newline);
334     }
335 
336     formattedWrite(app, "{");
337     writeEntry(db[$ - 1]);
338     formattedWrite(app, "}");
339 
340     formattedWrite(app, "]");
341 }